/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2011-2015 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://github.com/payara/Payara/blob/main/LICENSE.txt
 * See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at legal/OPEN-SOURCE-LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
// Portions Copyright [2016-2020] Payara Foundation and/or affiliates

package org.glassfish.admin.amx.impl.config;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.*;
import javax.management.Attribute;
import static org.glassfish.admin.amx.config.AMXConfigConstants.*;
import org.glassfish.admin.amx.config.AMXConfigProxy;
import org.glassfish.admin.amx.config.AttributeResolver;
import org.glassfish.admin.amx.core.AMXProxy;
import org.glassfish.admin.amx.core.Util;
import org.glassfish.admin.amx.impl.mbean.AMXImplBase;
import org.glassfish.admin.amx.impl.util.*;
import org.glassfish.admin.amx.util.*;
import org.glassfish.admin.amx.util.jmx.JMXUtil;
import static org.glassfish.external.amx.AMX.*;
import org.glassfish.external.arc.Stability;
import org.glassfish.external.arc.Taxonomy;
import org.jvnet.hk2.config.*;

/**
 * Base class from which all AMX Config MBeans should derive (but not "must").
 * <p>
 */
@Taxonomy(stability = Stability.NOT_AN_INTERFACE)
public class AMXConfigImpl extends AMXImplBase {

    private final ConfigBean mConfigBean;

    private static final Logger logger = AMXLoggerInfo.getLogger();

    /**
     * MBeanInfo derived from the AMXConfigProxy interface, always the same
     */
    private static MBeanInfo configMBeanInfo;

    private static synchronized MBeanInfo getAMXConfigMBeanInfo() {
        if (configMBeanInfo == null) {
            configMBeanInfo = MBeanInfoSupport.getMBeanInfo(AMXConfigProxy.class);
        }
        return configMBeanInfo;
    }

    /**
     * We save time and space by creating exactly one MBeanInfo for any given config interface; it can be shared among all instances since it is invariant.
     */
    private static final ConcurrentMap<Class<? extends ConfigBeanProxy>, MBeanInfo> mInfos
            = new ConcurrentHashMap<Class<? extends ConfigBeanProxy>, MBeanInfo>();

    private static MBeanInfo createMBeanInfo(final ConfigBean cb) {
        Class<? extends ConfigBeanProxy> intf = cb.getProxyType();
        MBeanInfo newInfo = mInfos.get(intf);
        if (newInfo != null) {
            return newInfo;
        }

        final ConfigBeanJMXSupport spt = ConfigBeanJMXSupportRegistry.getInstance(cb);
        final MBeanInfo info = spt.getMBeanInfo();

        final List<MBeanAttributeInfo> attrInfos = ListUtil.newListFromArray(info.getAttributes());
        final MBeanInfo spiInfo = MBeanInfoSupport.getAMX_SPIMBeanInfo();

        // make a list so we can remove "Children" attribute if this MBean cannot have any
        final List<MBeanAttributeInfo> spiAttrInfos = ListUtil.newListFromArray(spiInfo.getAttributes());
        if (spt.isLeaf()) {
            JMXUtil.remove(spiAttrInfos, ATTR_CHILDREN);
        }

        // Add in the AMX_SPI attributes, replacing any with the same name
        for (final MBeanAttributeInfo attrInfo : spiAttrInfos) {
            // remove existing info
            final String attrName = attrInfo.getName();
            final MBeanAttributeInfo priorAttrInfo = JMXUtil.remove(attrInfos, attrName);

            // special case the Name attribute to preserve its metadata
            if (attrName.equals(ATTR_NAME) && priorAttrInfo != null) {
                final Descriptor mergedD = JMXUtil.mergeDescriptors(attrInfo.getDescriptor(), priorAttrInfo.getDescriptor());

                final MBeanAttributeInfo newAttrInfo = new MBeanAttributeInfo(attrName,
                        attrInfo.getType(), attrInfo.getDescription(), attrInfo.isReadable(), attrInfo.isWritable(), attrInfo.isIs(), mergedD);

                attrInfos.add(newAttrInfo);
            } else {
                attrInfos.add(attrInfo);
            }
        }

        final List<MBeanOperationInfo> operationInfos = ListUtil.newListFromArray(info.getOperations());
        operationInfos.addAll(ListUtil.newListFromArray(getAMXConfigMBeanInfo().getOperations()));

        final MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[attrInfos.size()];
        attrInfos.toArray(attrs);

        final MBeanOperationInfo[] operations = new MBeanOperationInfo[operationInfos.size()];
        operationInfos.toArray(operations);

        newInfo = new MBeanInfo(
                info.getClassName(),
                info.getDescription(),
                attrs,
                info.getConstructors(),
                operations,
                info.getNotifications(),
                info.getDescriptor());

        MBeanInfo oldInfo = mInfos.putIfAbsent(intf, newInfo);

        return oldInfo != null ? oldInfo : newInfo;
    }

    public AMXConfigImpl(
            final ObjectName parentObjectName,
            final ConfigBean configBean) {
        super(parentObjectName, createMBeanInfo(configBean));

        mConfigBean = configBean;

        // eager initialization, it will be needed momentarily
        getConfigBeanJMXSupport();
    }

    @Override
    protected void setAttributeManually(final Attribute attr)
            throws AttributeNotFoundException, InvalidAttributeValueException {
        final AttributeList attrList = new AttributeList();
        attrList.add(attr);

        try {
            final AttributeList successList = setAttributesInConfigBean(attrList);
            if (successList.isEmpty()) {
                throw new AttributeNotFoundException(attr.getName());
            }
        } catch (final Exception e) {
            // propogate the stack trace back, it's important for clients to have somethingto go on
            final Throwable rootCause = ExceptionUtil.getRootCause(e);
            throw new AttributeNotFoundException(ExceptionUtil.toString(rootCause));
        }
    }

    /**
     * Note that the default implementation sets attributes one at a time, but that MBeans with transactional requirements (eg configuration) may wish to set
     * them as a group.
     */
    @Override
    public AttributeList setAttributes(final AttributeList attrs) {
        try {
            return setAttributesTransactionally(attrs);
        } catch (final Exception e) {
            // squelch, per JMX spec
        }

        // return an empty list, per JMX spec for failure
        return new AttributeList();
    }

    public AttributeList setAttributesTransactionally(final AttributeList attrs) throws Exception {
        final AttributeList successList = new AttributeList();

        try {
            final AttributeList delegateSuccess = setAttributesInConfigBean(attrs);
            successList.addAll(delegateSuccess);
        } catch (final Exception e) {
            // propogate the stack trace back, it's important for clients to have something to go on
            final Throwable rootCause = ExceptionUtil.getRootCause(e);

            // do not propagate back any proprietary exception; class might not exist on client
            throw new Exception(ExceptionUtil.toString(rootCause));
        }

        return successList;
    }

    /**
     * The actual name could be different than the 'name' property in the ObjectName if it contains characters that are illegal for an ObjectName. Also, there
     * can be a Name attribute which is not a key value.
     */
    @Override
    public String getName() {
        final ConfigBean cb = getConfigBean();

        String name = AMXConfigLoader.getKey(cb);
        if (name == null) {
            // deal with annoying and rare case of name existing, but not a key value
            name = cb.rawAttribute("name");
        }

        return name == null ? NO_NAME : name;
    }

    private ConfigBean getConfigBean() {
        return mConfigBean;
    }

    private ConfigBeanProxy getConfigBeanProxy() {
        return getConfigBean().getProxy(getConfigBean().getProxyType());
    }

    /**
     * Resolve a template String. See {@link AttributeResolver} for details.
     */
    public String resolveAttributeValue(final String varString) {
        if (!AttributeResolverHelper.needsResolving(varString)) {
            return varString;
        }

        return new AttributeResolverHelper().resolve(varString);
    }

    public String resolveAttribute(final String attrName) {
        try {
            final Object value = getAttribute(attrName);
            return resolveAttributeValue(value == null ? null : "" + value);
        } catch (final AttributeNotFoundException e) {
            logger.log(Level.SEVERE, AMXLoggerInfo.attributeNotfound, new Object[]{attrName, getObjectName()});
            return null;
        }
    }

    public Boolean resolveBoolean(final String attrName) {
        return Boolean.parseBoolean(resolveAttribute(attrName));
    }

    public Integer resolveInteger(final String attrName) {
        return Integer.parseInt(resolveAttribute(attrName));
    }

    public Long resolveLong(final String attrName) {
        return Long.parseLong(resolveAttribute(attrName));
    }

    public AttributeList resolveAttributes(final String[] attrNames) {
        Issues.getAMXIssues().notDone("resolveAttributes: use annotations to create the correct type");

        final AttributeList attrs = getAttributes(attrNames);
        final AttributeList resolvedAttrs = new AttributeList();
        for (final Object o : attrs) {
            Attribute r = (Attribute) o;
            // allow non-String attributes
            final Object value = r.getValue();
            if ((value instanceof String) && AttributeResolverHelper.needsResolving((String) value)) {
                final String resolvedValue = resolveAttributeValue((String) value);
                // TODO: use annotation to determine correct type
                r = new Attribute(r.getName(), resolvedValue);
            }

            resolvedAttrs.add(r);
        }

        return resolvedAttrs;
    }

//========================================================================================
    /**
     * Parameters for creating one or more children, each of which can (recursively) contain other descendants.
     */
    static class CreateParams {

        final String mType;
        final Map<String, Object> mAttrs;
        final List<CreateParams> mChildren;

        public CreateParams(final String type, final Map<String, ?> values) {
            mAttrs = MapUtil.newMap();
            mChildren = ListUtil.newList();
            mType = type;

            if (values == null) {
                return; // null is legal, no attributes
            }

            for (final Map.Entry<String, ?> me : values.entrySet()) {
                final String nameAsProvided = me.getKey();
                final String xmlName = ConfigBeanJMXSupport.toXMLName(nameAsProvided);  // or type
                final Object value = me.getValue();

                if (value == null
                        || (value instanceof String)
                        || (value instanceof Number)
                        || (value instanceof Boolean)) {
                    // auto-convert specific basic types to String
                    final String valueString = value == null ? null : "" + value;
                    mAttrs.put(xmlName, valueString);
                } else if (value instanceof String[]) {
                    // A String[] is always mapped to a List<String>
                    mAttrs.put(xmlName, ListUtil.asStringList(value));
                } else if (value instanceof Map) {
                    // one sub-element whose type is its key in the containing Map
                    final Map<String, Object> m = TypeCast.checkMap(Map.class.cast(value), String.class, Object.class);
                    final CreateParams child = new CreateParams(xmlName, m);
                    mChildren.add(child);
                } else if (value instanceof Map[]) {
                    // one or more sub elements whose type is its key in the containing Map
                    final Map[] maps = (Map[]) value;
                    for (final Map m : maps) {
                        final Map<String, Object> mTyped = TypeCast.checkMap(m, String.class, Object.class);
                        final CreateParams child = new CreateParams(xmlName, mTyped);
                        mChildren.add(child);
                    }
                } else {
                    throw new IllegalArgumentException("Value of class " + value.getClass().getName() + " not supported for attribute " + nameAsProvided);
                }
            }
        }

        public String              type()     { return mType; }
        public String              name()     { return (String)mAttrs.get("name"); }
        public Map<String,Object>  attrs()    { return Collections.unmodifiableMap(mAttrs); }
        public List<CreateParams>  children() { return Collections.unmodifiableList(mChildren); }

        /**
         * Convert incoming attributes to HK2 requirements.
         */
        List<AttributeChanges>
                toAttributeChanges(final Map<String, Object> values) {
            if (values == null) {
                return null;
            }

            final List<AttributeChanges> changes = ListUtil.newList();
            for (Map.Entry<String, Object> xmlEntry : mAttrs.entrySet()) {
                final Object value = xmlEntry.getValue();
                if (value instanceof String) {
                    changes.add(new ConfigSupport.SingleAttributeChange(xmlEntry.getKey(), (String) value));
                } else {
                    // what about String[]?
                    throw new IllegalArgumentException();
                }
            }
            return changes;
        }

        public String toString(final String prefix) {
            final StringBuilder buf = new StringBuilder();
            final String NL = StringUtil.LS;

            // crude toString, really should indent
            buf.append(prefix).append(mType).append(" = ").append(mAttrs).append(NL);
            if (!mChildren.isEmpty()) {
                buf.append(prefix).append("[");
                for (final CreateParams child : mChildren) {
                    buf.append(child.toString("    " + prefix)).append(NL);
                }
                buf.append(prefix).append(']');
            }

            return buf.toString();
        }

        @Override
        public String toString() {
            return toString("");
        }
    }

    /**
     * To make error messages more friendly and quick sanity check, verify that no conflicting children already exist.
     */
    private void
            checkForConflicts(final List<CreateParams> children) {
        final Map<String, Map<String, AMXProxy>> existingChildren = getSelf().childrenMaps();
        for (final CreateParams params : children) {
            final String type = params.type();
            final Map<String, AMXProxy> childrenOfType = existingChildren.get(type);
            if (childrenOfType != null) {
                // children of this type exist, check that there is no conflicting child already
                final AMXProxy firstChild = childrenOfType.values().iterator().next();
                if (firstChild.extra().singleton()) {
                    throw new IllegalArgumentException("Singleton child of type " + type + " already exists.");
                }
                if (childrenOfType.get(params.name()) != null) {
                    throw new IllegalArgumentException("Child of type " + type + " named " + params.name() + " already exists.");
                }
            }
        }
    }

    ObjectName[] createChildren(final List<CreateParams> children, final Map<String, Object> attrs) {
        debug(children.toString());
        checkForConflicts(children);

        final ConfigBeanProxy parent = getConfigBeanProxy();
        final ChildrenCreator creator = new ChildrenCreator(children, attrs);
        try {
            ConfigSupport.apply(creator, parent);
        } catch (Exception e) {
            AMXLoggerInfo.getLogger().log(Level.INFO, AMXLoggerInfo.cantCreateChildren, e);
            throw new RuntimeException(e);
        }

        // ensure that all new ConfigBeans have been registered as MBeans
        final List<ObjectName> newMBeans = ListUtil.newList();
        final List<ConfigBean> newDescendants = creator.configBeans();

        final AMXConfigLoader amxLoader = SingletonEnforcer.get(AMXConfigLoader.class);
        for (final ConfigBean newDescendant : newDescendants) {
            amxLoader.handleConfigBean(newDescendant, true);
            final ObjectName objectName = ConfigBeanRegistry.getInstance().getObjectName(newDescendant);
            newMBeans.add(objectName);
        }

        return CollectionUtil.toArray(newMBeans, ObjectName.class);
    }

    public ObjectName[]
            createChildren(
                    final Map<String, Map<String, Object>[]> childrenMaps,
                    final Map<String, Object> attrs) {
        final List<CreateParams> children = ListUtil.newList();

        for (Map.Entry<String, Map<String, Object>[]> entry : childrenMaps.entrySet()) {
            for (final Map<String, Object> m : entry.getValue()) {
                children.add(new CreateParams(entry.getKey(), m));
            }
        }

        return createChildren(children, attrs);
    }

    /**
     * Create one or more children
     */
    private final class ChildrenCreator implements ConfigCode {

        private final List<CreateParams> mChildrenMaps;
        private final Map<String, Object> mAttrs;
        private final List<ConfigBean> mNewConfigBeans;

        ChildrenCreator(final List<CreateParams> childrenMaps, final Map<String, Object> attrs) {
            mChildrenMaps = childrenMaps;
            mAttrs = attrs;
            mNewConfigBeans = ListUtil.newList();
        }

        @Override
        public Object run(final ConfigBeanProxy... params)
                throws PropertyVetoException, TransactionFailure {
            if (params.length != 1) {
                throw new IllegalArgumentException();
            }
            final ConfigBeanProxy parent = params[0];

            final ConfigBean source = (ConfigBean) ConfigBean.unwrap(parent);
            final ConfigSupport configSupport = source.getHabitat().getService(ConfigSupport.class);

            return _run(parent, configSupport);
        }

        public Object _run(
                final ConfigBeanProxy parent,
                final ConfigSupport configSupport)
                throws PropertyVetoException, TransactionFailure {
            final WriteableView parentW = WriteableView.class.cast(Proxy.getInvocationHandler(Proxy.class.cast(parent)));

            // if attributes were specified, set them first.
            if (mAttrs != null) {
                setAttrs(parent, mAttrs);
            }

            final SubElementsCallback callback = new SubElementsCallback(mChildrenMaps);

            final ConfigBeanJMXSupport sptRoot = ConfigBeanJMXSupportRegistry.getInstance(Dom.unwrap(parent).getProxyType());
            final List<ConfigBean> newDescendants = callback.recursiveCreate(parentW, sptRoot, mChildrenMaps);
            mNewConfigBeans.addAll(newDescendants);

            return null;
        }

        public List<ConfigBean> configBeans() {
            return mNewConfigBeans;
        }
    }

    public ObjectName createChild(final String type, final Map<String, Object> params) {
        final CreateParams childParams = new CreateParams(type, params);

        final List<CreateParams> children = ListUtil.newList();
        children.add(childParams);
        final ObjectName[] objectNames = createChildren(children, null);

        return objectNames[0];
    }

    /**
     * Replace "Name" or "name" with the
     */
    Map<String, Object>
            replaceNameWithKey(
                    final Map<String, Object> attrs,
                    final ConfigBeanJMXSupport spt) {
        String key = null;
        if (attrs.containsKey(ATTR_NAME)) {
            key = ATTR_NAME;
        } else if (attrs.containsKey("name")) {
            key = "name";
        }

        Map<String, Object> m = attrs;

        if (key != null) {
            // map "Name" or "name" to the actual key value (which could be "name')
            final String xmlKeyName = spt.getNameHint();
            // rename to the appropriate key name, if it doesn't already exist
            // eg there could be a non-key attribute "Name" and another key attribute; leave that alone
            if (xmlKeyName != null && !attrs.keySet().contains(xmlKeyName)) {
                m = new HashMap<String, Object>(attrs);
                final Object value = m.remove(key);
                m.put(xmlKeyName, value);
            }
        }

        return m;
    }

    /**
     * exists so we can get the parameterized return type
     */
    public static List<String> listOfString() {
        return null;
    }

    public static String convertAttributeName(final String s) {
        // do not alter any name that is already all lower-case or that contains a "-" */
        if (s.equals(s.toLowerCase(Locale.getDefault())) || s.indexOf('-') >= 0) {
            return (s);
        }

        // Dom.convertName() has a bug: IsFooBar => is-foo-bar, but is-foo-bar => -foo-bar.
        return Dom.convertName(s);
    }

    private void setAttrs(
            final ConfigBeanProxy target,
            final Map<String, Object> attrs) {
        final WriteableView targetW = WriteableView.class.cast(Proxy.getInvocationHandler(Proxy.class.cast(target)));

        for (final Map.Entry<String, Object> me : attrs.entrySet()) {
            final String attrName = me.getKey();
            final Object attrValue = me.getValue();
            final String xmlName = convertAttributeName(attrName);

            final ConfigBean targetCB = (ConfigBean) Dom.unwrap(target);
            final ConfigModel.Property modelProp = targetCB.model.findIgnoreCase(xmlName);
            if (modelProp == null) {
                throw new IllegalArgumentException("Can't find ConfigModel.Property for attr " + xmlName + " on " + targetCB.getProxyType());
            }
            if (modelProp.isCollection()) {
                java.lang.reflect.Method method;
                try {
                    method = getClass().getMethod("listOfString", new Class[0]);
                } catch (final NoSuchMethodException | SecurityException e) {
                    throw new IllegalStateException("impossible");
                }
                final java.lang.reflect.Type listOfStringClass = method.getGenericReturnType();

                List<String> list;
                if (attrValue instanceof String[]) {
                    list = ListUtil.asStringList(attrValue);
                } else {
                    list = TypeCast.checkList(TypeCast.asList(attrValue), String.class);
                }
                targetW.setter(modelProp, list, listOfStringClass);
            } else {
                targetW.setter(modelProp, attrValue, String.class);
            }
        }
    }

    /**
     * Callback to create sub-elements (recursively) on a newly created child element.
     */
    private final class SubElementsCallback implements TransactionCallBack<WriteableView> {

        private final List<CreateParams> mSubs;

        public SubElementsCallback(final List<CreateParams> subs) {
            mSubs = subs;
        }

        @Override
        public void performOn(final WriteableView item) throws TransactionFailure {
            final ConfigBeanJMXSupport sptRoot = ConfigBeanJMXSupportRegistry.getInstance(com.sun.enterprise.config.serverbeans.Domain.class);

            recursiveCreate(item, sptRoot, mSubs);
        }

        /**
         * If the child is of a type matching an @Element that is a List<its type>, then get that list and add it to it.
         */
        private void addToList(
                final WriteableView parent,
                final ConfigBeanProxy child) {
            final Class<? extends ConfigBeanProxy> parentClass = parent.getProxyType();
            final Class<? extends ConfigBeanProxy> childClass = Dom.unwrap(child).getProxyType();
            final ConfigBeanJMXSupport parentSpt = ConfigBeanJMXSupportRegistry.getInstance(parentClass);

            final ConfigBeanJMXSupport.ElementMethodInfo elementInfo = parentSpt.getElementMethodInfo(childClass);
            final ConfigBean parentBean = (ConfigBean) Dom.unwrap(parent.getProxy(parentClass));
            if (elementInfo != null && Collection.class.isAssignableFrom(elementInfo.method().getReturnType())) {
                // get the Collection and add the child
                final ConfigModel.Property modelProp = parentBean.model.findIgnoreCase(elementInfo.xmlName());
                final List list = (List) parent.getter(modelProp, elementInfo.method().getGenericReturnType());
                list.add(child);
            } else if (elementInfo != null) {
                final ConfigModel.Property modelProp = parentBean.model.findIgnoreCase(elementInfo.xmlName());
                if (modelProp == null) {
                    throw new IllegalArgumentException("Can't find ConfigModel.Property for \"" + elementInfo.xmlName() + "\"");
                }
                parent.setter(modelProp, child, childClass);
            }
        }

        private List<ConfigBean> recursiveCreate(
                final WriteableView parent,
                final ConfigBeanJMXSupport sptRoot,
                final List<CreateParams> subs) throws TransactionFailure {
            final List<ConfigBean> newChildren = ListUtil.newList();

            // create each sub-element, recursively
            for (final CreateParams childParams : subs) {
                final String type = childParams.type();

                final Class<? extends ConfigBeanProxy> clazz = ConfigBeanJMXSupportRegistry.getConfigBeanProxyClassFor(sptRoot, type);
                if (clazz == null) {
                    throw new IllegalArgumentException("@Configured interface for type " + type + " cannot be found");
                }

                final ConfigBeanJMXSupport spt = ConfigBeanJMXSupportRegistry.getInstance(clazz);

                final ConfigBeanProxy childProxy = parent.allocateProxy(clazz);
                Dom newBean = Dom.unwrap(childProxy);
                newBean.addDefaultChildren();
                addToList(parent, childProxy);
                final ConfigBean child = (ConfigBean) Dom.unwrap(childProxy);
                newChildren.add(child);
                final WriteableView childW = WriteableView.class.cast(Proxy.getInvocationHandler(Proxy.class.cast(childProxy)));

                final Map<String, Object> childAttrs = replaceNameWithKey(childParams.attrs(), spt);
                setAttrs(childProxy, childAttrs);

                if (!childParams.children().isEmpty()) {
                    final List<ConfigBean> more = recursiveCreate(childW, spt, childParams.children());
                    newChildren.addAll(more);
                }
            }
            return newChildren;
        }
    }

    public ObjectName removeChild(final String type) {
        final ObjectName child = child(type);
        if (child == null) {
            logger.log(Level.SEVERE, AMXLoggerInfo.childNotfound, type);
            return null;
        }

        return remove(child);
    }

    public ObjectName removeChild(final String type, final String name) {
        final ObjectName child = child(type, name);
        if (child == null) {
            return null;
        }

        return remove(child);
    }

    private ObjectName remove(final ObjectName childObjectName) {
        ObjectName removed = null;
        try {
            final ConfigBean childConfigBean = ConfigBeanRegistry.getInstance().getConfigBean(childObjectName);

            try {
                ConfigSupport.deleteChild(this.getConfigBean(), childConfigBean);
                removed = childObjectName;
            } catch (final TransactionFailure tf) {
                throw new RuntimeException("Transaction failure deleting " + JMXUtil.toString(childObjectName), tf);
            }

            // NOTE: MBeans unregistered asynchronously by AMXConfigLoader
            // enforce synchronous semantics to clients by waiting until this happens
            //  the listener is smart enough not to wait if it's already unregistered
            final UnregistrationListener myListener = new UnregistrationListener(getMBeanServer(), childObjectName);
            final long TIMEOUT_MILLIS = 10 * 1000;
            final boolean unregisteredOK = myListener.waitForUnregister(TIMEOUT_MILLIS);
            if (!unregisteredOK) {
                throw new RuntimeException("Something went wrong unregistering MBean " + JMXUtil.toString(childObjectName));
            }
        } catch (final Exception e) {
            throw new RuntimeException("Problem deleting " + childObjectName, e);
        }
        return removed;
    }

    private Object invokeDuckMethod(
            final ConfigBeanJMXSupport.DuckTypedInfo info,
            Object[] args)
            throws MBeanException {
        try {

            if (!info.method().getDeclaringClass().isAssignableFrom(getConfigBeanProxy().getClass())) {
                throw new IllegalArgumentException("invokeDuckMethod: " + getConfigBean().getProxyType() + " not asssignable to " + info.method().getDeclaringClass());
            }

            Object result = info.method().invoke(getConfigBeanProxy(), args);
            result = translateResult(result);

            return result;
        } catch (final Exception e) {
            throw new MBeanException(e);
        }
    }

    private ObjectName getObjectName(final ConfigBeanProxy cbp) {
        final Dom dom = Dom.unwrap(cbp);

        if (dom instanceof ConfigBean) {
            return ConfigBeanRegistry.getInstance().getObjectName((ConfigBean) dom);
        }

        // we can't return a Dom over the wire
        return null;
    }

    /**
     * Convert results that contain local ConfigBeanProxy into ObjectNames. Ignore other items, passing through unchanged.
     */
    private Object translateResult(final Object result) {
        // short-circuit the common case
        if (result instanceof String) {
            return result;
        }

        Object out = result;

        // ConfigBean types must be mapped back to ObjectName; they can't go across the wire
        if (result instanceof ConfigBeanProxy) {
            out = getObjectName((ConfigBeanProxy) result);
        } else if (result instanceof Collection) {
            final Collection<Object> c = (Collection) result;
            final Collection<Object> translated = new ArrayList<Object>();
            for (final Object item : c) {
                translated.add(translateResult(item));
            }

            if (result instanceof Set) {
                out = new HashSet<Object>(translated);
            } else if (result instanceof AbstractQueue) {
                out = new LinkedBlockingDeque(translated);
            } else {
                out = translated;
            }
        } else if (result instanceof Map) {
            final Map resultMap = (Map) result;
            Map outMap = new HashMap();
            for (final Object meo : resultMap.entrySet()) {
                Map.Entry me = (Map.Entry) meo;
                outMap.put(translateResult(me.getKey()), translateResult(me.getValue()));
            }
            out = outMap;
        } else if (result.getClass().isArray()) {
            final Class<?> componentType = result.getClass().getComponentType();
            if (ConfigBeanProxy.class.isAssignableFrom(componentType)) {
                final Object[] items = (Object[]) result;
                final ObjectName[] objectNames = new ObjectName[items.length];
                for (int i = 0; i < items.length; ++i) {
                    objectNames[i] = getObjectName((ConfigBeanProxy) items[i]);
                }
                out = objectNames;
            }
        }

        return out;
    }

    /**
     * Automatically figure out get<abc>Factory(), create<Abc>Config(), remove<Abc>Config().
     *
     */
    @Override
    protected Object invokeManually(
            String operationName,
            Object[] args,
            String[] types)
            throws MBeanException, ReflectionException, NoSuchMethodException, AttributeNotFoundException {
        Object result = null;
        debugMethod(operationName, args);

        ConfigBeanJMXSupport.DuckTypedInfo duckTypedInfo = null;
        if ((duckTypedInfo = getConfigBeanJMXSupport().findDuckTyped(operationName, types)) != null) {
            result = invokeDuckMethod(duckTypedInfo, args);
        } else {
            result = super.invokeManually(operationName, args, types);
        }
        return result;
    }

    public void sendConfigCreatedNotification(final ObjectName configObjectName) {
        sendNotification(CONFIG_CREATED_NOTIFICATION_TYPE,
                CONFIG_REMOVED_NOTIFICATION_TYPE,
                CONFIG_OBJECT_NAME_KEY, configObjectName);
    }

    public void sendConfigRemovedNotification(final ObjectName configObjectName) {
        sendNotification(CONFIG_REMOVED_NOTIFICATION_TYPE,
                CONFIG_REMOVED_NOTIFICATION_TYPE,
                CONFIG_OBJECT_NAME_KEY, configObjectName);
    }

    private ConfigBeanJMXSupport getConfigBeanJMXSupport() {
        return ConfigBeanJMXSupportRegistry.getInstance(getConfigBean());
    }

    private static Map<String, String> getDefaultValues(final Class<? extends ConfigBeanProxy> intf, boolean useAMXAttributeNames) {
        return ConfigBeanJMXSupportRegistry.getInstance(intf).getDefaultValues(useAMXAttributeNames);
    }

    public final Map<String, String> getDefaultValues(final String type, final boolean useAMXAttributeNames) {
        final Class<? extends ConfigBeanProxy> intf = getConfigBeanProxyClassForContainedType(type);

        return getDefaultValues(intf, useAMXAttributeNames);
    }

    public final Map<String, String> getDefaultValues(final boolean useAMXAttributeNames) {
        return getDefaultValues(mConfigBean.getProxyType(), useAMXAttributeNames);
    }

    private Class<? extends ConfigBeanProxy> getConfigBeanProxyClassForContainedType(final String type) {
        final ConfigBeanJMXSupport spt = getConfigBeanJMXSupport();

        return ConfigBeanJMXSupportRegistry.getConfigBeanProxyClassFor(spt, type);
    }

    @Override
    protected String[] attributeNameToType(final String attributeName) {
        return new String[]{
            Util.typeFromName(attributeName), attributeName
        };
    }

    @Override
    protected Object getAttributeManually(final String name)
            throws AttributeNotFoundException, ReflectionException, MBeanException {
        return getAttributeFromConfigBean(name);
    }

//-------------------------------------------------------------
    /**
     * Get an Attribute. This is a bit tricky, because the target can be an XML attribute, an XML string element, or an XML list of elements.
     */
    protected final Object getAttributeFromConfigBean(final String amxName) {
        Object result = null;

        final MBeanAttributeInfo attrInfo = getAttributeInfo(amxName);
        if (attrInfo == null) {
            //
            // check for  PSEUDO ATTTRIBUTES implemented as methods eg getFoo()
            //

            ConfigBeanJMXSupport.DuckTypedInfo info = getConfigBeanJMXSupport().findDuckTyped("get" + amxName, null);
            if (info == null) {
                info = getConfigBeanJMXSupport().findDuckTyped("is" + amxName, null);
            }
            if (info != null) {
                try {
                    result = invokeDuckMethod(info, null);
                    return result;
                } catch (final Exception e) {
                    throw new RuntimeException(new MBeanException(e, amxName));
                }

            }
            throw new RuntimeException(new AttributeNotFoundException(amxName));
        }
        final String xmlName = ConfigBeanJMXSupport.xmlName(attrInfo, amxName);
        final boolean isAttribute = ConfigBeanJMXSupport.isAttribute(attrInfo);

        if (isAttribute) {
            result = mConfigBean.rawAttribute(xmlName);
        } else if (ConfigBeanJMXSupport.isElement(attrInfo)) {
            if (String.class.getName().equals(attrInfo.getType())) {
                final List<?> leaf = mConfigBean.leafElements(xmlName);
                if (leaf != null) {
                    try {
                        result = leaf.get(0);
                    } catch (final Exception e) {
                        // doesn't exist, return null
                    }
                }
            } else if (attrInfo.getType().equals(String[].class.getName())) {

                final List<?> leaf = mConfigBean.leafElements(xmlName);
                if (leaf != null) {
                    // verify that it is List<String> -- no other types are supported in this way
                    final List<String> elems = TypeCast.checkList(leaf, String.class);
                    result = CollectionUtil.toArray(elems, String.class);
                }
            } else {
                throw new IllegalArgumentException("getAttributeFromConfigBean: unsupported return type: " + attrInfo.getType());
            }
        }
        return result;
    }

    private static final class MyTransactionListener implements TransactionListener {

        private final List<PropertyChangeEvent> mChangeEvents = new ArrayList<PropertyChangeEvent>();

        private final ConfigBean mTarget;

        MyTransactionListener(final ConfigBean target) {
            mTarget = target;
        }

        @Override
        public void transactionCommited(List<PropertyChangeEvent> changes) {
            // include only events that match the desired config bean; other transactions
            // could generate events on other ConfigBeans. For that matter, it's unclear
            // why more than one transaction on the same ConfigBean couldn't be "heard" here.
            for (final PropertyChangeEvent event : changes) {
                final Object source = event.getSource();
                if (source instanceof ConfigBeanProxy) {
                    final Dom dom = Dom.unwrap((ConfigBeanProxy) source);
                    if (dom instanceof ConfigBean && mTarget == (ConfigBean) dom) {
                        mChangeEvents.add(event);
                    }
                }
            }
        }

        @Override
        public void unprocessedTransactedEvents(List<UnprocessedChangeEvents> changes) {
            // amx probably does not care that some changes were not processed successfully
            // and will require a restart
        }

        List<PropertyChangeEvent> getChangeEvents() {
            return mChangeEvents;
        }

    }

    private void joinTransaction(final Transaction t, final WriteableView writeable)
            throws TransactionFailure {
        if (!writeable.join(t)) {
            t.rollback();
            throw new TransactionFailure("Cannot enlist " + writeable.getProxyType() + " in transaction", null);
        }
    }

    private static void commit(final Transaction t)
            throws TransactionFailure {
        try {
            t.commit();
        } catch (final RetryableException e) {
            t.rollback();
            throw new TransactionFailure(e.getMessage(), e);
        } catch (final TransactionFailure e) {
            t.rollback();
            throw e;
        }
    }

    static <T extends ConfigBeanProxy> WriteableView getWriteableView(final T s, final ConfigBean sourceBean)
            throws TransactionFailure {
        final WriteableView f = new WriteableView(s);
        if (sourceBean.getLock().tryLock()) {
            return f;
        }
        throw new TransactionFailure("Config bean already locked " + sourceBean, null);
    }

    private static Type getCollectionGenericType() {
        try {
            return ConfigSupport.class.getDeclaredMethod("defaultPropertyValue", (Class[]) null).getGenericReturnType();
        } catch (NoSuchMethodException e) {
            // not supposed to happen, throw any reasonabl exception
            throw new IllegalArgumentException();
        }
    }

    /**
     * Handle an update to a collection, returning the List<String> that results.
     */
    private List<String> handleCollection(
            final WriteableView writeable,
            final ConfigModel.Property prop,
            final List<String> argValues) {
        final Object o = writeable.getter(prop, getCollectionGenericType());
        final List<String> masterList = TypeCast.checkList(TypeCast.asList(o), String.class);

        masterList.retainAll(argValues);
        for (final String s : argValues) {
            if (!masterList.contains(s)) {
                masterList.add(s);
            }
        }

        return new ArrayList<String>(masterList);
    }

    private class Applyer {

        final Transaction mTransaction;

        final ConfigBean mConfigBean;

        final WriteableView mWriteable;

        public Applyer(final ConfigBean cb) throws TransactionFailure {
            this(cb, new Transaction());
        }

        public Applyer(final ConfigBean cb, final Transaction t)
                throws TransactionFailure {
            mConfigBean = cb;
            mTransaction = t;

            final ConfigBeanProxy readableView = cb.getProxy(cb.getProxyType());
            mWriteable = getWriteableView(readableView, cb);
        }

        protected void makeChanges()
                throws TransactionFailure {
        }

        final void apply()
                throws TransactionFailure {
            try {
                joinTransaction(mTransaction, mWriteable);

                makeChanges();

                commit(mTransaction);
            } finally {
                mConfigBean.getLock().unlock();
            }
        }

    }

    protected ConfigModel.Property getConfigModel_Property(final String xmlName) {
        final ConfigModel.Property cmp = mConfigBean.model.findIgnoreCase(xmlName);
        if (cmp == null) {
            throw new IllegalArgumentException("Illegal name: " + xmlName);
        }
        return cmp;
    }

    private final class MakeChangesApplyer extends Applyer {

        private final Map<String, Object> mChanges;

        public MakeChangesApplyer(
                final ConfigBean cb,
                final Map<String, Object> changes)
                throws TransactionFailure {
            super(cb);
            mChanges = changes;
        }

        @Override
        protected void makeChanges() throws TransactionFailure {
            for (Map.Entry<String, Object> xmlEntry : mChanges.entrySet()) {
                final Object value = xmlEntry.getValue();
                final ConfigModel.Property prop = getConfigModel_Property(xmlEntry.getKey());

                if (prop.isCollection()) {
                    handleCollection(mWriteable, prop, ListUtil.asStringList(value));
                } else if (value == null || (value instanceof String)) {
                    mWriteable.setter(prop, value, String.class);
                } else {
                    throw new TransactionFailure("Illegal data type for attribute " + xmlEntry.getKey() + ": " + value.getClass().getName());
                }
            }
        }

    }

    private Map<String, Object> mapNamesAndValues(
            final Map<String, Object> amxAttrs,
            final Map<String, Object> noMatch) {
        final Map<String, Object> xmlAttrs = new HashMap<String, Object>();

        final Map<String, MBeanAttributeInfo> attrInfos = getAttributeInfos();

        for (final Map.Entry<String, Object> me : amxAttrs.entrySet()) {
            final String amxAttrName = me.getKey();
            final Object valueIn = me.getValue();

            final MBeanAttributeInfo attrInfo = attrInfos.get(amxAttrName);
            if (attrInfo == null) {
                debug("WARNING: setAttributes(): no MBeanAttributeInfo found for: " + amxAttrName);
                noMatch.put(amxAttrName, valueIn);
                continue;
            }
            final String xmlName = ConfigBeanJMXSupport.xmlName(attrInfo, amxAttrName);

            if (xmlName != null) {

                final Object value = valueIn;

                // We accept only Strings, String[] or null
                if (valueIn == null || (value instanceof String)) {
                    xmlAttrs.put(xmlName, (String) value);
                } else {
                    final ConfigModel.Property prop = getConfigModel_Property(xmlName);
                    if (prop != null && prop.isCollection()) {
                        if ((valueIn instanceof String[]) || (valueIn instanceof List)) {
                            xmlAttrs.put(xmlName, ListUtil.asStringList(valueIn));
                        } else {
                            noMatch.put(amxAttrName, valueIn);
                        }
                    } else {
                        noMatch.put(amxAttrName, valueIn);
                    }
                }
            } else {
                debug("WARNING: setAttributes(): no xmlName match found for AMX attribute: " + amxAttrName);
                noMatch.put(amxAttrName, valueIn);
            }
        }

        return xmlAttrs;
    }

    public AttributeList setAttributesInConfigBean(final AttributeList attrsIn) throws TransactionFailure {
        // now map the AMX attribute names to xml attribute names
        final Map<String, Object> amxAttrs = JMXUtil.attributeListToValueMap(attrsIn);
        final Map<String, Object> notMatched = new HashMap<String, Object>();
        final Map<String, Object> xmlAttrs = mapNamesAndValues(amxAttrs, notMatched);

        if (!notMatched.keySet().isEmpty()) {
            debug("setAttributes: failed to map these AMX attributes: {" + CollectionUtil.toString(notMatched.keySet(), ", ") + "}");

        }

        final AttributeList successfulAttrs = new AttributeList();

        final Transactions transactions = mConfigBean.getHabitat().getService(Transactions.class);

        if (!xmlAttrs.isEmpty()) {

            final MyTransactionListener myListener = new MyTransactionListener(mConfigBean);
            transactions.addTransactionsListener(myListener);

            // results should contain only those that succeeded which will be all or none
            // depending on whether the transaction worked or not
            try {
                final MakeChangesApplyer mca = new MakeChangesApplyer(mConfigBean, xmlAttrs);
                mca.apply();

                // use 'attrsIn' vs 'attrs' in case not all values are 'String'
                successfulAttrs.addAll(attrsIn);
            } catch (final TransactionFailure tf) {
                // empty results -- no Exception should be thrown per JMX spec
                debug(ExceptionUtil.toString(tf));
                throw (tf);
            } finally {
                transactions.waitForDrain();

                transactions.removeTransactionsListener(myListener);
            }
        }

        return successfulAttrs;
    }

    /**
     * Share one sequence number for *all* Config MBeans to keep overhead low instead of
     */
    private static final AtomicLong sSequenceNumber = new AtomicLong(0);

    void issueAttributeChangeForXmlAttrName(final String xmlAttrName, final String message, final Object oldValue, final Object newValue,
            final long whenChanged) {

        final Map<String, String> m = getConfigBeanJMXSupport().getFromXMLNameMapping();
        final String attributeName = m.containsKey(xmlAttrName) ? m.get(xmlAttrName) : xmlAttrName;
        if (attributeName.equals(xmlAttrName)) // will *always* be different due to camel case
        {
            logger.log(Level.SEVERE, AMXLoggerInfo.attributeNotfound, new Object[] { xmlAttrName, getObjectName() });
        }

        final String attributeType = String.class.getName();

        getLogger().log(Level.FINE, "{0} -- {1} = {2} <== {3}", new Object[]{getObjectName(), attributeName, newValue, oldValue});

        final long sequenceNumber = sSequenceNumber.getAndIncrement();
        final AttributeChangeNotification notif
                = new AttributeChangeNotification(getObjectName(), sequenceNumber, whenChanged, message, attributeName, attributeType, oldValue, newValue);

        sendNotification(notif);

    }
}
